import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Date;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ConcurrentHashMap;

public class Server {
	
	private static final int BLOCKING_QUEUE_CAPACITY = 1024;
	public static final int GET_COMMAND = 0;
	public static final int DELETE_COMMAND = 1;
	public static final int INSERT_COMMAND = 2;
	public static final int UPDATE_COMMAND = 3;
	public static final int REPAIR_COMMAND = 4;
	
	public static final int ONE_LEVEL = 1;
	public static final int ALL_LEVEL = 9;

	public static ServerProfile profile;

	private static Hashtable<Integer, KeyValuePair> tuples = new Hashtable<>();
	
	public static ConcurrentHashMap<Long, ArrayBlockingQueue<Message>> respMap = new ConcurrentHashMap<>();
	public static ArrayBlockingQueue<SearchResponseMessage> searchResponseQueue = new ArrayBlockingQueue<>(1024);
	
	public static void main(String[] args) throws IOException, InterruptedException {
		if (args.length != 1) {
			System.err.println("Incorrect number of parameters: Server [name]");
			System.exit(1);
		}
		profile = Configuration.getInstance().getServerByName(args[0]);
		System.out.println("Starting server " + profile.getName());
		new Listener(profile);
		boolean ok;
		while (true) {
//			System.out.print("Enter Command: ");
			BufferedReader bufferRead = new BufferedReader(new InputStreamReader(System.in));
			String s = bufferRead.readLine();
			if (!s.matches("(insert|update).[0-9]+.[A-Za-z]+.[1|9]|"
					+ "(delete.[0-9]+)|"
					+ "(get.[0-9]+.[1|9])|"
					+ "(show-all)|"
					+ "(search).[0-9]+")) {
				System.err.println("Incorrect Command");
				continue;
			}
			ok = doCommand(s);
			if (ok) {
				System.out.println("Done!");
			} else {
				System.err.println("Failed!");
			}
		}
	}
	
	private static boolean doCommand(String s) throws InterruptedException {
		String[] strArray = s.split(" ");
		
		if (strArray.length < 1) {
			System.err.println("Invalid Command! Format: [Get | Insert | Update | Delete] [Key | Key Value] [Level]");
			return false;
		}
		
		String command = strArray[0].toLowerCase();
		
		switch (command) {
			case "delete": {
				if (strArray.length < 2) {
					System.err.println("Invalid command! Format: Delete Key");
					return false;
				}
				int key = Integer.parseInt(strArray[1]);
				return doDelete(key);
			}
			case "get": {
				if (strArray.length < 3) {
					System.err.println("Invalid command! Format: Get Key Level");
					return false;
				}
				int key = Integer.parseInt(strArray[1]);
				int level = Integer.parseInt(strArray[2]);
				return doGet(key, level);
			}
			case "insert": {
				if (strArray.length < 4) {
					System.err.println("Invalid command! Format: Insert Key Value Level");
					return false;
				}
				int key = Integer.parseInt(strArray[1]);
				String value = strArray[2];
				int level = Integer.parseInt(strArray[3]);
				return doInsert(key, value, level);
			}
			case "update": {
				if (strArray.length < 4) {
					System.err.println("Invalid command! Format: Update Key Value Level");
					return false;
				}
				int key = Integer.parseInt(strArray[1]);
				String value = strArray[2];
				int level = Integer.parseInt(strArray[3]);
				return doUpdate(key, value, level);
			}
			case "search": {
				if (strArray.length < 2) {
					System.err.println("Invalid command! Format: Search Key");
					return false;
				}
				int key = Integer.parseInt(strArray[1]);
				return doSearch(key);
			}
			case "show-all": {
				return doShowAll();
			}
			default: {
				System.err.println("Invalid Command! Format: [Get | Insert | Update | Delete] [Key | Key Value] [Level]");
				return false;
			}
		}
	}
	
	private static ServerProfile[] getRecipientsByKey(int key) {
		
		int hash = key % Configuration.getInstance().getServerProfiles().size();
		ServerProfile[] recepients = new ServerProfile[3];
		int counter = 0;
		for (int i = 0; i < 4; i++) {
			if (i == hash) {
				continue;
			}
			recepients[counter++] = Configuration.getInstance().getServerByName(""+i);
		}	
		return recepients;
	}
	
	private static ArrayBlockingQueue<Message> registerResponse(long msgID) {
		ArrayBlockingQueue<Message> q = new ArrayBlockingQueue<>(BLOCKING_QUEUE_CAPACITY);
		respMap.put(msgID, q);
		return respMap.get(msgID);
	}
	
	private static KeyValuePair getResponse(ArrayBlockingQueue<Message> q, int level) throws InterruptedException {
		KeyValuePair mostUpdated = null;
		if (level == ALL_LEVEL) {
			ArrayList<Message> tmp = new ArrayList<>();
			for (int i = 0; i < Configuration.getInstance().getNumOfServers()-1; i++) {
				ResponseMessage r = (ResponseMessage) q.take();
//				System.out.println("Server got a response for message " + r.getId() + " from " + r.getSender());
				tmp.add(r);
//				System.out.println("======");
//				System.out.println(r);
//				System.out.println(r.getTuple());
//				System.out.println(r.getTuple().getTime());
//				if (mostUpdated!=null) System.out.println(mostUpdated.getTime());
//				System.out.println("=======");
				if (!r.isSuccessful()) {
					return null;
				}
				if (mostUpdated == null || mostUpdated.getTime().after(r.getTuple().getTime())) {
					mostUpdated = r.getTuple();
				}
			}
			for (int i = 0; i < Configuration.getInstance().getNumOfServers()-1; i++) {
				q.add(tmp.get(i));
			}
			
		} else if (level == ONE_LEVEL) {
			ResponseMessage r = null;
			ArrayList<ResponseMessage> res = new ArrayList<>();
			while (r==null) {
				r = (ResponseMessage) q.take();
				res.add(r);
			}
			for (ResponseMessage re: res) {
				q.add(re);
			}
			mostUpdated = r.getTuple();
		}
		return mostUpdated;
	}
	
	private static boolean doDelete(int key) throws InterruptedException {
		Message msg = new RequestMessage(profile, getRecipientsByKey(key), DELETE_COMMAND, new KeyValuePair(key, null, new Date()));
		ArrayBlockingQueue<Message> q = sendToReplicas(msg);
		KeyValuePair r = getResponse(q, ALL_LEVEL);
		return (r!=null);
	}

	private static boolean doGet(int key, int level) throws InterruptedException {
		Message msg = new RequestMessage(profile, getRecipientsByKey(key), GET_COMMAND, new KeyValuePair(key, null, new Date()));
		ArrayBlockingQueue<Message> q = sendToReplicas(msg);
		KeyValuePair r = getResponse(q, level);
		new Repair(q);
		return (r!=null);
	}
	
	private static boolean doInsert(int key, String value, int level) throws InterruptedException {
		Message msg = new RequestMessage(profile, getRecipientsByKey(key), INSERT_COMMAND, new KeyValuePair(key, value, new Date()));
		ArrayBlockingQueue<Message> q = sendToReplicas(msg);
		KeyValuePair r = getResponse(q, level);
		return (r!=null);
	}
	
	private static boolean doUpdate(int key, String value, int level) throws InterruptedException {
		Message msg = new RequestMessage(profile, getRecipientsByKey(key), UPDATE_COMMAND, new KeyValuePair(key, value, new Date()));
		ArrayBlockingQueue<Message> q = sendToReplicas(msg);
		KeyValuePair r = getResponse(q, level);
		return (r!=null);
	}
	
	private static ArrayBlockingQueue<Message> sendToReplicas(Message msg) {
		ArrayBlockingQueue<Message> q = registerResponse(msg.getId());
		for (ServerProfile r: msg.getRecipients()) {
			new Sender(msg, r);
		}
		return q;
	}
	
	private static boolean doShowAll() {
		System.out.println("This serveer contains the following key-value pairs:");
		Enumeration<KeyValuePair> it = tuples.elements();
		while (it.hasMoreElements()) {
			KeyValuePair tuple = (KeyValuePair) it.nextElement();
			System.out.println(tuple);
		}
		return true;
	}
	
	private static boolean doSearch(int key) throws InterruptedException {
		System.out.println("Servers contains key " + key + " are: ");
		int size = Configuration.getInstance().getServerProfiles().size();
		SearchMessage sMsg = new SearchMessage(Server.profile, getRecipientsByKey(key), key);
		for (ServerProfile r: sMsg.getRecipients()) {
			new Sender(sMsg, r);
		}
		for (int i = 0; i < size-1; i++) {
			SearchResponseMessage srMsg = searchResponseQueue.take();
			System.out.println("Server " + srMsg.getSender().getName() + " contains key");
		}
		return true;
	}
	
	public static KeyValuePair localCommand(KeyValuePair tuple, int command) {
		switch (command) {
		case DELETE_COMMAND:
			return localDelete(tuple.getKey());
		case GET_COMMAND:
			return localGet(tuple.getKey());
		case INSERT_COMMAND:
			return localInsert(tuple);
		case UPDATE_COMMAND:
			return localUpdate(tuple);
		case REPAIR_COMMAND:
			return localRepair(tuple);
		default:
			return null;
		}
	}
	
	public static KeyValuePair localDelete(int key) {
		if (tuples.containsKey(key)) {
			KeyValuePair t = tuples.get(key);
			tuples.remove(key);
			System.out.println("Deleted " + t);
			return t;
		} else {
			System.err.println("LocalDeleting...The key " + key + " does not exist!");
			return null;
		}
	}
	
	public static KeyValuePair localGet(int key) {
		if (tuples.containsKey(key)) {
			KeyValuePair t = tuples.get(key);
			System.out.println("Got " + t);
			return t;
		} else {
			System.err.println("LocalGetting...The key " + key + " does not exist!");
			return null;
		}
	}
	
	public static KeyValuePair localInsert(KeyValuePair tuple) {
		if (!tuples.containsKey(tuple.getKey())) {
			tuples.put(tuple.getKey(), tuple);
			System.out.println("Inserted " + tuple);
			return tuple;
		} else {
			System.err.println("LocalInserting...The key " + tuple.getKey() + " exists!");
			return null;
		}
	}
	
	public static KeyValuePair localUpdate(KeyValuePair tuple) {
		if (tuples.containsKey(tuple.getKey())) {
			if (tuples.get(tuple.getKey()).getValue().equals(tuple.getValue())) {
				System.out.println("LocalUpdating...value is the same");
			}
			tuples.put(tuple.getKey(), tuple);
			System.out.println("Updated " + tuple);
			return tuple;
		} else {
			System.err.println("LocalUpdating...The key " + tuple.getKey() + " does not exist!");
			return null;
		}
	}
	
	public static KeyValuePair localRepair(KeyValuePair tuple) {
		
		int key = tuple.getKey();
		KeyValuePair kvp;
		if (tuples.containsKey(key)) {
			kvp = localUpdate(tuple);
		} else {
			kvp = localInsert(tuple);
		}
		
		return kvp;
	}
	
	public static boolean localSearch(int key) {
		if (tuples.containsKey(key) && tuples.get(key) != null) {
			return true;
		} else {
			return false;
		}
	}
	
	public static void printTuples() {
		System.out.print("Printing Tuples...");
		for (int i: tuples.keySet()) {
			System.out.print(tuples.get(i) + " ");
		}
		System.out.println();
	}
}